###################################################
#    This file is part of RPAWL.
#
#    RPAWL is free software: you can redistribute it and/or modify
#    it under the terms of the GNU General Public License as published by
#    the Free Software Foundation, either version 3 of the License, or
#    (at your option) any later version.
#
#    RPAWL is distributed in the hope that it will be useful,
#    but WITHOUT ANY WARRANTY; without even the implied warranty of
#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#    GNU General Public License for more details.
#
#    You should have received a copy of the GNU General Public License
#    along with RPAWL.  If not, see <http://www.gnu.org/licenses/>.
###################################################
## Function to check if the flat histogram criterion is reached
checkFlatHistogram <- function(FHbincount, binning){
  difference <- abs((FHbincount / sum(FHbincount)) - binning@desiredfreq)
  FHreached <- all(difference < binning@fhthreshold * binning@desiredfreq) &
  all(FHbincount > 10)
  return(FHreached)
}


## Particle Wang-Landau function with flat histogram criterion
## AP stands for Algorithmic Parameters


#'Parallel Adaptive Wang-Landau
#'
#'Implements the Parallel Adaptive Wang-Landau algorithm.
#'
#'
#'@param target Object of class \code{\link{target}}: specifies the target
#'distribution.  See the help of \code{\link{target}}. If the target is
#'discrete, target must contain the slots \code{dproposal}, \code{rproposal}
#'and \code{proposalparam} that specify the proposal kernel in the
#'Metropolis-Hastings step. Otherwise the default is an adaptive gaussian
#'random walk.
#'@param binning Object of class \code{\link{binning}}, defining the initial
#'bins used by the Wang-Landau algorithm.  The binning object also contains
#'some parameters specifying if the automatic binning mechanism is active or
#'not, for instance.
#'@param AP Object of class \code{\link{tuningparameters}}: specifies the
#'number of chains, the number of iterations, and what should be stored during
#'along the run. See the help of \code{\link{tuningparameters}}.
#'@param proposal Object of class \code{\link{proposal}}: specifies the
#'proposal distribution to be used to propose new values and to compute the
#'acceptance rate. See the help of \code{\link{proposal}}. If this is not
#'specified and the target is continuous, then the default is an adaptive
#'gaussian random walk.
#'@param verbose Object of class \code{"logical"}: if TRUE (default) then
#'prints some indication of progress in the console.
#'@return The function returns a list holding various information:
#'
#'and other quantities, that you can browse by calling \code{"names(results)"}
#'where \code{"results"} is the output of the function.
#'\itemize{
#'\item finalchains The last point of each chain.
#'\item acceptrates The vector of acceptance rates at each step.
#'\item sigma The vector of the standard deviations used by the MH kernel
#'along the iterations. If the proposal was adaptive, this allows to check how
#'the adaptation behaved.
#'\item allchains If asked in the tuning parameters, the chain history.
#'\item alllogtarget If asked in the tuning parameters, the associated
#'log density evaluations.
#'\item meanchains If asked in the tuning parameters, the mean
#'(component-wise) of each chain.
#'\item logthetahistory If asked in the tuning parameters, all the log
#'theta penalties.
#'}
#'@author Luke Bornn <bornn@@stat.harvard.edu>, Pierre E. Jacob
#'<pierre.jacob.work@@gmail.com>
#'@seealso \code{\link{adaptiveMH}, \link{binning}}
#'@keywords ~kwd1 ~kwd2
pawl <- function(target, binning, AP, proposal, verbose = TRUE){
    if (verbose) print("Launching Particle Wang-Landau algorithm ...") 
    # Init some algorithmic parameters ...
    nbins <- length(binning@bins)
    # The bias is saved in a matrix but because
    # the dimension might change (if bins are split)
    # we also store a list of matrices of possibly different
    # dimensions, if bins have been split.
    logtheta <- matrix(ncol = nbins, nrow = AP@niterations + 1)
    logtheta[1,] <- 0
    logthetahistory <- list()
    # keep track of the count of visits to each bin
    # since the last flat histogram
    if (binning@useFH)
        FHbincount <- rep(0, nbins)
    # We compute the bins' middles in case the automatic bin split
    # mechanism is enabled. We also keep the count of visits in the bins,
    # as well as the count of visits in the half left part of the bins ...
    binning@binmids <- getBinMiddles(binning)
    diagnoseactive <- binning@autobinning
    if (diagnoseactive){
        diagnosebincount <- rep(0, nbins)
        diagnosehalfleftcount <- rep(0, nbins - 2)
    }
    # k is the temperature of the stochastic approximation tempering,
    # that makes the update of the bias less and less important. k is
    # increased when the flat histogram criterion is met.
    k <- 1
    khistory <- c(k)
    FHtimes <- c()
    # Due to the bin split mechanism, we keep track of the bins ...
    splitTimes <- c()
    binshistory <- list()
    binshistory[[1]] <- binning@bins
    nbinsvector <- c(nbins)
    # We keep track of the chains, and of the history in "allchains"...
    chains <- as.matrix(target@rinit(AP@nchains))
    # We keep track of the log densities computed along the iterations ...
    currentlogtarget <- target@logdensity(chains, target@parameters)
    if (AP@computemean){
        sumchains <- matrix(0, nrow = AP@nchains, ncol = target@dimension)
    }
    acceptrates <- rep(0, AP@niterations)
    # Setting the proposal distribution for the MH kernel
    if (missing(proposal) & target@type == "continuous"){
        proposal <- createAdaptiveRandomWalkProposal(nchains = AP@nchains, 
                                               targetdimension = target@dimension,
                                               adaptiveproposal = TRUE)
    }
    if (proposal@adaptiveproposal){
        sigma <- rep(0, AP@niterations + 1)
        sigma[1] <- proposal@proposalparam$sigma
    }
    dproposal <- proposal@dproposal
    rproposal <- proposal@rproposal
    proposalparam <- proposal@proposalparam
    # standard dev of the adaptive proposal ...
    if (proposal@adaptiveproposal){
        sigma <- rep(0, AP@niterations + 1)
        sigma[1] <- proposal@proposalparam$sigma
    }
    if (target@type == "discrete" & proposal@adaptiveproposal){
        proposal@adaptiveproposal <- FALSE
        if (verbose) cat("switching off adaptive proposal, because the target is discrete\n")
    }
    # We compute the locations of the chains, that is, in which 
    # bins they are.
    currentreaction <- binning@position(chains, currentlogtarget)
    currentlocations <- binning@getLocations(binning@bins, currentreaction)
    thisiterationcount <- tabulate(currentlocations, nbins = nbins)
    if (binning@useFH)
        FHbincount <- FHbincount + thisiterationcount
    if (diagnoseactive){
      diagnosebincount <- thisiterationcount
      diagnosehalfleftcount <- getInnerLeftCounts(binning,
                                                  currentreaction, 
                                                  currentlocations)
    }
    lastFHtime <- 0
    lastSplittime <- 0
    if (AP@saveeverynth > 0){
      nallchains <- 1 + floor((AP@niterations) / AP@saveeverynth)
      if (verbose) cat("saving every", AP@saveeverynth, "iterations\n")
      totalsize <- nallchains * AP@nchains * target@dimension
      if (verbose)
          cat("hence saving a vector of size", nallchains, "x", AP@nchains, 
              "x", target@dimension, "=", totalsize, "\n")
      if (totalsize > 10^8){
        if (verbose) cat("which bigger than 10^8: you better have a lot of memory available!!\n")
        suggestedmaxnallchains <- floor((10^8) / (target@dimension * AP@nchains)) + 1
        if (verbose) cat("you can maybe set saveeverynth to something bigger than ", 
            floor(AP@niterations / suggestedmaxnallchains) + 1, "\n")
        if (verbose) cat("type anything to continue, or Ctrl-C to abort\n")
        y<-scan(n=1)
      }
      allchains <- array(NA, dim = c(nallchains, AP@nchains, target@dimension))
      nstoredchains <- 1
      allchains[nstoredchains,,] <- chains
      alllogtarget <- matrix(NA, nrow = nallchains, ncol = AP@nchains)
      alllogtarget[nstoredchains,] <- currentlogtarget
    }
    allreaction <- matrix(nrow = AP@niterations + 1, ncol = AP@nchains)
    allreaction[1,] <- currentreaction
    iterstep <- max(100, AP@niterations / 50)
    for (iteration in 1:AP@niterations){
        if (!(iteration %% iterstep)){
          if (verbose) cat("iteration", iteration, "/", AP@niterations, "\n")
        }
        ## Sample new values from the MH kernel targeting the biased distribution ...
        rproposalresults <- rproposal(chains, proposalparam)
        proposals <- rproposalresults$states
        if (target@updateavailable){
            proposalLogTarget <- currentlogtarget + target@logdensityupdate(chains, 
                                              target@parameters, rproposalresults$others)
        } else {
            proposalLogTarget <- target@logdensity(proposals, target@parameters)
        }
        proposalReaction <- binning@position(proposals, proposalLogTarget)
        proposalLocations <- binning@getLocations(binning@bins, proposalReaction)
        loguniforms <- log(runif(AP@nchains))

        accepts <- (loguniforms < ((proposalLogTarget + dproposal(proposals, chains, proposalparam)
                                    - logtheta[iteration - lastSplittime,][proposalLocations]) 
        - (currentlogtarget + dproposal(chains, proposals, proposalparam)
           - logtheta[iteration - lastSplittime,][currentlocations ])))
        chains[accepts,] <- proposals[accepts,]
        currentlogtarget[accepts] <- proposalLogTarget[accepts]
        currentlocations[accepts] <- proposalLocations[accepts]
        currentreaction[accepts] <- proposalReaction[accepts]
        if (AP@saveeverynth > 0 & iteration %% AP@saveeverynth == 0){
            nstoredchains <- nstoredchains + 1
            allchains[nstoredchains,,] <- chains
            alllogtarget[nstoredchains,] <- currentlogtarget
        }
        allreaction[iteration + 1,] <- currentreaction
        if (AP@computemean && iteration > AP@computemeanburnin){
            sumchains <- sumchains + chains
        }
        acceptrates[iteration] <- mean(accepts)
        ## update the proportions of visit in each bin
        ## and the inner distribution of the chains in each bin
        ## (proportions in the left hand side of each bin)
        thisiterationcount <- tabulate(currentlocations, nbins = nbins)
        if (binning@useFH)
            FHbincount <- FHbincount + thisiterationcount
        if (diagnoseactive){
            diagnosebincount <- diagnosebincount + thisiterationcount
            diagnosehalfleftcount <- diagnosehalfleftcount + getInnerLeftCounts(binning, currentreaction, currentlocations)
        }
        ## update the bias using all the chains ...
        currentproportions <- thisiterationcount / AP@nchains
        if (binning@useLearningRate){
            if (binning@useFH){
                logtheta[iteration + 1 - lastSplittime,] <- logtheta[iteration - 
                             lastSplittime,] + binning@learningrate(k) * 
                             (currentproportions - binning@desiredfreq)
            } else {
                logtheta[iteration + 1 - lastSplittime,] <- logtheta[iteration - 
                             lastSplittime,] + binning@learningrate(iteration) * 
                             (currentproportions - binning@desiredfreq)
            }
        } else {
            logtheta[iteration + 1 - lastSplittime,] <- logtheta[iteration - lastSplittime,] + 
                    (currentproportions - binning@desiredfreq)
        }
        ## update the adaptive proposal standard deviation ...
        if (proposal@adaptiveproposal){
            sigma[iteration + 1] <- max(10^(-10 - target@dimension), sigma[iteration] + 
                proposal@adaptationrate(iteration) * (2 * (mean(accepts) > 0.234) - 1))
            proposalparam$sigma <- sigma[iteration + 1]
        } 
        if (diagnoseactive & binning@alongenergy){
            if (diagnosebincount[nbins] > AP@nchains){
                if (verbose) cat("right end bin reached: disactivating bin diagnosis\n")
                diagnoseactive <- FALSE
            }
        }
        if (diagnoseactive & (iteration %% max(100, 1 + floor(AP@niterations / 100)) == 0) &
            iteration < AP@niterations){
            if (verbose) cat("/** diagnosis at iteration", iteration, "\n")
            if (verbose) cat("* current number of bins =", nbins, "\n")
            allproportions <- diagnosebincount / sum(diagnosebincount)
            foundbins <- findSkewedBins(binning, 
                                         diagnosehalfleftcount, diagnosebincount)
            skewedbins <- foundbins$skewedbins + 1
            problem_bins <- (binning@desiredfreq - allproportions) / binning@desiredfreq
            if (verbose) cat("desired freq - proportions:\n", (problem_bins), "\n")
            if (verbose) cat("* skewed bins:", skewedbins, "\n")
            if (verbose) cat("* bins with enough points to be split:", 
                which(diagnosebincount > 10 * AP@nchains), "\n")
            bintosplit <- c()
            if (binning@alongenergy){
                for (binindex in 2:(nbins - 1)){
                    if (diagnosebincount[binindex] > 10 * AP@nchains){
                        if (all(problem_bins[(binindex + 1):nbins] > 0.9)){
                            bintosplit <- c(bintosplit, binindex)
                        }
                    }  
                }
            } else {
                for (binindex in 2:(nbins - 1)){
                    if (diagnosebincount[binindex] > 10 * AP@nchains &
                        binindex %in% skewedbins){
                        if (binning@smoothbinning | problem_bins[binindex + 1] > 0.9 |
                            problem_bins[binindex - 1] > 0.9){
                            bintosplit <- c(bintosplit, binindex)
                        }
                    }  
                }
            }
            if (!is.null(bintosplit)){
                if (verbose) cat("* bins to split:", bintosplit, "\n")
                newcuts <- binning@binmids[bintosplit - 1]
                foundbins <- list(binsToSplit = bintosplit, newcuts = newcuts)
                splitresults <- binsplitter(binning, foundbins, 
                    logtheta[iteration + 1 - lastSplittime,], binning@desiredfreq, 
                    binning@alongenergy)
                newbins <- splitresults$newbins
                nbins <- length(newbins)
                binning@bins <- newbins
                binning@binmids <- getBinMiddles(binning)
                binning@desiredfreq <- splitresults$newdesiredfreq
                if (binning@useFH){
                    k <- 1
                    khistory[length(khistory)] <- 1
                    FHbincount <- rep(0, nbins)
                }
                diagnosebincount <- rep(0, nbins)
                diagnosehalfleftcount <- rep(0, nbins - 2)
                # update the current values that depend on bins
                currentreaction <- binning@position(chains, currentlogtarget)
                currentlocations <- binning@getLocations(binning@bins, 
                                                         currentreaction)
                # store things
                nbinsvector <- c(nbinsvector, nbins)
                binshistory[[length(binshistory) + 1]] <- newbins
                lastSplittime <- iteration
                splitTimes <- c(splitTimes, iteration)
                if (length(splitTimes) == 1){
                    logthetahistory[[1]] <- logtheta[1:(iteration + 1),]
                } else {
                    diffSplitTimes <- splitTimes[length(splitTimes)] - 
                        splitTimes[length(splitTimes) - 1]
                    logthetahistory[[length(splitTimes)]] <- 
                        logtheta[1:(diffSplitTimes + 1),]
                }
                logtheta <- matrix(ncol = nbins, nrow = AP@niterations + 1 - iteration)
                logtheta[1,] <- splitresults$newthetas
            }
            if (verbose) cat("*/\n")
        }

        ## check if flat histogram is reached ...
        if (binning@useFH){
            FHreached <- checkFlatHistogram(FHbincount, binning)
            enoughElapsedTime <- (iteration >= lastFHtime + binning@minSimEffort)
            if (FHreached && enoughElapsedTime){
                if (verbose) cat("Flat histogram criterion met at iteration", iteration, "!\n")
                k <- k + 1
                khistory <- c(khistory, k)
                FHtimes <- c(FHtimes, iteration)
                lastFHtime <- iteration
                if (!binning@smoothbinning)
                  diagnoseactive <- FALSE
                FHbincount <- rep(0, nbins)
            }
        }
    }
    results <- list(chains = chains, acceptrates = acceptrates, logtheta = logtheta,
                finallocations = currentlocations, FHtimes = FHtimes, 
                finalbins = binning@bins, finaldesiredfreq = binning@desiredfreq,
                splitTimes = splitTimes, nbins = nbinsvector,
                binshistory = binshistory, khistory = khistory)
    if (proposal@adaptiveproposal)
        results$sigma <- sigma
    if (AP@saveeverynth > 0){
        results$allchains <- allchains
        results$alllogtarget <- alllogtarget
    }
    results$allreaction <- allreaction
    if (AP@computemean){
        results$meanchains <- sumchains / (AP@niterations - AP@computemeanburnin)
    }
    logthetahistory[[length(logthetahistory) + 1]] <- logtheta
    results$logthetahistory <- logthetahistory
    return(results)
}



#'Observed Frequencies in each bin.
#'
#'This function provides a convenient way to check whether the target
#'frequencies have been reached.  Since new bins can be created during the
#'algorithm, this function aggregates them in the right way so that the user
#'can know if the initial bins (on which the desired frequencies were
#'specified) have been visited enough.
#'
#'
#'@param results Object of class \code{"list"}: either the output of
#'\code{\link{pawl}} or of \code{\link{adaptiveMH}}.
#'@param binning Object of class \code{\link{binning}}: the binning on which
#'the frequencies have to be computed.
#'@return The function is supposed to be used for the lines that it prints in
#'the console.  However it also returns a vector of sampling frequencies
#'corresponding to the initial bins.
#'@author Luke Bornn <bornn@@stat.harvard.edu>, Pierre E. Jacob
#'<pierre.jacob.work@@gmail.com>
#'@seealso \code{\link{pawl}}
getFrequencies <- function(results, binning){
    allproportions <- tabulate(binning@getLocations(results$finalbins, 
                      c(results$allreaction)), nbins = length(results$finalbins))
    finalfrequencies <- allproportions / sum(allproportions)
    innerfinalbins <- results$finalbins
    innerfinalbins <- innerfinalbins[2:(length(innerfinalbins))]
    innerinitbins <- results$binshistory[[1]]
    innerinitbins <- innerinitbins[2:(length(innerinitbins))]
    samplefrequencies <- c(finalfrequencies[1])
    for (index in 1:(length(innerinitbins)-1)){
        indexstart <- which(innerinitbins[index] == innerfinalbins)
        indexstop <- which(innerinitbins[index+1] == innerfinalbins)
        samplefrequencies <- c(samplefrequencies, sum(finalfrequencies[(1+indexstart):indexstop]))
    }
    samplefrequencies <- c(samplefrequencies, finalfrequencies[length(finalfrequencies)])
    cat("/* Frequency check\n")
    cat("*Do the obtained frequencies match the desired frequencies?\n")
    cat("*final bins:", results$finalbins, "\n")
    cat("*corresponding frequencies:", finalfrequencies, "\n")
    cat("*initial bins:", results$binshistory[[1]], "\n")
    cat("*desired frequencies: ", binning@desiredfreq, "\n")
    cat("*obtained frequencies:", samplefrequencies, "\n")
    return(samplefrequencies)
}



